import https from 'https'
import * as fs from 'node:fs'
import url from 'url'
import path from 'path'
import { exec } from 'node:child_process'

const INPUT_PATH = './input'

const VIDEO_PATH = './video'

const DATA_PATH = './data'

let tsList, curVideoName, curNum, prefix
const downFailList = new Set() // 收集下载失败的ts
const playInfo = {} // 临时存储播放信息

!fs.existsSync(INPUT_PATH) && fs.mkdirSync(INPUT_PATH)
!fs.existsSync(VIDEO_PATH) && fs.mkdirSync(VIDEO_PATH)
!fs.existsSync(DATA_PATH) && fs.mkdirSync(DATA_PATH)

// 防抖
const debounce = (callback, delay) => {
  let timer
  return () => {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      callback()
    }, delay)
  }
}
// 任务调度器
const promiseLimit = (limit, callback) => {
  let running = 0
  const queue = []
  const enqueue = (task) => {
    queue.push(task)
    _next()
  }
  const _next = () => {
    if (running >= limit || queue.length === 0) {
      if (!queue.length && callback) {
        callback()
      }
      return
    }
    running++
    const task = queue.shift()
    task()
      .then(() => {
        // console.log(`Task ${task} completed`)
      })
      .catch(() => {
        // console.log(`Task ${task} failed`)
      })
      .finally(() => {
        running--
        _next()
      })
  }
  return { enqueue }
}

// sse消息发送
const sseData = (res, event, id, data, retry) => {
  res.write(`event: ${event}\n`) // 事件类型
  res.write(`id: ${id}\n`) // 消息 ID
  res.write(`data: ${data}\n`) // 消息数据
  if (retry) res.write(`retry: ${retry}\n`) // 重连时间
  res.write('\n\n') // 消息结束
}

// 下载ts
const downloadTS = (tsUrl, ts) => () => {
  const { hostname, port, pathname: path } = new URL(tsUrl)
  const options = {
    hostname,
    port,
    path,
    headers: {
      Accept: '*/*',
      Origin: 'https://zj.jsjinfu.com:8443',
      Priority: 'u=1, i',
      'User-Agent':
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
    },
  }
  return new Promise((resolve, reject) => {
    const req = https
      .request(options, (res) => {
        const file = fs.createWriteStream(`${INPUT_PATH}/` + ts)
        res.pipe(file)
        file.on('finish', () => {
          file.close(resolve)
          if (downFailList.has(ts)) downFailList.delete(ts)
        })
      })
      .on('error', (err) => {
        downFailList.add(ts)
        reject(err)
      })
      .end()
    req.setTimeout(10000, () => {
      req.abort() // 终止请求
      console.log(tsUrl + '请求超时')
    })
  })
}

// 获取ts地址列表
const getTsList = (url, response) => {
  return new Promise((resolve, reject) => {
    https
      .get(url, (res) => {
        res.setEncoding('utf8')
        let data = ''
        res.on('data', (chunk) => {
          data += chunk
        })
        res.on('end', async () => {
          // 检查是否是最终index.m3u8文件
          if (data.includes('#EXT-X-STREAM-INF')) {
            //非最终index.m3u8文件
            let realM3u8 = data
              .split('\n')
              .find((item) => item.replace('\r', '').endsWith('.m3u8'))
            let realUrl = url.replace('index.m3u8', '') + realM3u8
            prefix = realUrl.split('/').slice(0, -1).join('/') + '/'
            const resData = await getTsList(realUrl, response)
            return resolve(resData)
          }

          sseData(response, 'done', new Date(), 'index.m3u8文件获取完成', 10000)
          sseData(
            response,
            'doing',
            new Date(),
            '准备保存index.m3u8文件到本地...',
            10000
          )
          fs.writeFile(`${INPUT_PATH}/index.m3u8`, data, (err) => {
            if (err) {
              return console.error(err)
            }
            sseData(
              response,
              'done',
              new Date(),
              'index.m3u8文件保存完成',
              10000
            )
            sseData(response, 'doing', new Date(), '开始解析ts列表...', 10000)
            tsList = data
              .split('\n')
              .filter((line) => line.endsWith('.ts') || line.endsWith('.jpeg'))
            resolve(tsList)
          })
        })
      })
      .on('error', (err) => {
        reject(err)
      })
  })
}

// 请求并解析url地址拿到集数数组
const urlParse = (url) => {
  const { hostname, port, pathname: path } = new URL(url)
  const options = {
    hostname,
    port,
    path,
    headers: {
      Accept: '*/*',
      'User-Agent':
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
    },
  }
  return new Promise((resolve, reject) => {
    https
      .request(options, (res) => {
        res.setEncoding('utf8')
        let data = ''
        res.on('data', (chunk) => {
          data += chunk
        })
        res.on('end', () => {
          const match = unescape(data.match(/unescape\('(.*?)'\);/)[1])
          const result = match
            .split('#')
            .map(
              (item) => 'https://zj.jsjinfu.com:8443/?url=' + item.split('$')[1]
            )
          resolve(result)
        })
      })
      .on('error', reject)
      .end()
  })
}

// 解析m3u8的地址
const m3u8Parse = (url) => {
  const { hostname, port, pathname, search } = new URL(url)
  const options = {
    hostname,
    port,
    path: pathname + search,
    headers: {
      Accept:
        'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
      'Cache-Control': 'max-age=0',
      'Upgrade-Insecure-Requests': '1',
      Priority: 'u=0, i',
      'Sec-Ch-Ua':
        '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"',
      'User-Agent':
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
    },
  }
  return new Promise((resolve, reject) => {
    https
      .request(options, (res) => {
        res.setEncoding('utf8')
        let data = ''
        res.on('data', (chunk) => {
          data += chunk
        })
        res.on('end', () => {
          const match = data.match(/var video_url = '(.*?)';/)[1]
          resolve(match)
        })
      })
      .on('error', reject)
      .end()
  })
}

// 删除文件夹中的文件
const deleteDirSync = (dir) => {
  const fileList = fs.readdirSync(dir)
  fileList.forEach((file) => {
    const filePath = dir + '/' + file
    fs.unlinkSync(filePath) // 仅删除文件
  })
}

// 生成固定格式文件名 0001.ts
const generateName = (index, fix) =>
  `${index.toString().padStart(4, '0')}.${fix}`

const combine = (videoName, fileName, response, callback) => {
  // 处理m3u8内容
  const content = fs.readFileSync(`${INPUT_PATH}/index.m3u8`, 'utf8')
  const strContent = content
    .split('\n')
    .map((item) => {
      if (item.endsWith('.ts') || item.endsWith('.jpeg')) {
        // 将线上文件处理为本地文件同时修改格式
        return item.split('/').slice(-1)[0].split('.')[0] + '.ts'
      } else {
        return item
      }
    })
    .join('\n')

  fs.writeFileSync(`${INPUT_PATH}/index.m3u8`, strContent, 'utf8')

  // 重命名格式
  const fileList = fs.readdirSync(INPUT_PATH)
  fileList.forEach((item) => {
    if (!item.endsWith('.m3u8')) {
      fs.renameSync(
        `${INPUT_PATH}/${item}`,
        `${INPUT_PATH}/${item.split('.')[0] + '.ts'}`
      )
    }
  })

  exec(
    `ffmpeg -i ${INPUT_PATH}/index.m3u8 -c copy ${VIDEO_PATH}/${videoName}/${fileName}`,
    (error, stdout, stderr) => {
      if (error) {
        console.error(`执行出错: ${error}`)
        return
      }
      console.log(`stdout: ${stdout}`)
      sseData(
        response,
        'done',
        new Date(),
        `<span style="color: green">【${videoName}】第${Number(
          fileName.split('.')[0]
        )}集合并完成</span>`,
        10000
      )
      callback()
    }
  )
}

// 递归下载
const downing = (res, tsData, prefix, callback) => {
  sseData(res, 'doing', new Date(), 'ts资源下载中...', 10000)
  const task = tsData.map((item) =>
    downloadTS(
      item.startsWith('http') ? item : prefix + item,
      item.startsWith('http') ? item.split('/').slice(-1)[0] : item
    )
  ) // 全部压入任务队列
  const promise = promiseLimit(
    10,
    debounce(() => {
      if (downFailList.size) {
        sseData(
          res,
          'warning',
          new Date(),
          `下载结束，失败任务数：${downFailList.size}`,
          10000
        )
        downing(res, Array.from(downFailList), prefix, callback)
      } else {
        sseData(res, 'done', new Date(), 'ts资源下载完成', 10000)
        sseData(res, 'doing', new Date(), '开始合并ts...', 10000)
        combine(curVideoName, generateName(curNum, 'mp4'), res, callback)
      }
    }, 12000)
  )
  task.forEach((t) => promise.enqueue(t))
}

// 遍历执行下载任务
const queueExcute = async (res, index, downList) => {
  curNum = index + 1
  sseData(res, 'doing', new Date(), 'index.m3u8地址获取中...', 10000)
  const m3u8Url = await m3u8Parse(downList[index])
  sseData(res, 'done', new Date(), 'index.m3u8地址获取完成', 10000)

  sseData(res, 'doing', new Date(), '开始请求index.m3u8文件...', 10000)
  prefix = m3u8Url.replace('index.m3u8', '')
  const tsData = await getTsList(m3u8Url, res)
  sseData(res, 'done', new Date(), 'ts列表解析完成', 10000)

  downing(res, tsData, prefix, () => {
    sseData(res, 'doing', new Date(), '开始删除ts文件', 10000)
    deleteDirSync(INPUT_PATH)
    sseData(res, 'done', new Date(), 'ts文件删除完成', 10000)
    if (index + 1 === downList.length) {
      // 如果是最后一次就不继续
      sseData(
        res,
        'done',
        new Date(),
        '<span style="color: blue">当前任务已全部完成</span>',
        10000
      )
      sseData(res, 'over', new Date(), '任务结束', 10000)
      return
    }
    index += 1
    queueExcute(res, index, downList)
  }) // 下载ts
}

const excute = async (res, path) => {
  const parseUrl = url.parse(path, true)
  const { url: originUrl, name: videoName } = parseUrl.query

  curVideoName = videoName

  // 服务器声明接下来发送的是事件流
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    Connection: 'keep-alive',
    'Access-Control-Allow-Origin': '*',
  })

  // 发送注释保持长连接
  setInterval(() => {
    res.write(': \n\n')
  }, 12000)

  try {
    // 每次请求先清空input文件夹
    deleteDirSync(INPUT_PATH)

    if (!fs.existsSync(`${VIDEO_PATH}/${videoName}`)) {
      return sseData(res, 'over', new Date(), `没有该目录，请先新建目录`, 10000)
    }

    // // 创建视频名称目录
    // fs.mkdir(`${VIDEO_PATH}/${videoName}`, () => {
    //   sseData(res, 'done', new Date(), `【${videoName}】目录创建完成`, 10000)
    // })

    sseData(res, 'doing', new Date(), 'url解析中...', 10000)
    const result = await urlParse(originUrl)
    sseData(res, 'done', new Date(), 'url解析完成', 10000)

    // 判断是否已存在该视频目录
    sseData(res, 'doing', new Date(), '检索是否已录入...', 10000)
    const fileList = fs.readdirSync(`${VIDEO_PATH}/${videoName}`)
    if (fileList.length) {
      if (fileList.length === result.length) {
        sseData(
          res,
          'done',
          new Date(),
          '<span style="color: red">该视频已录入，且无更新资源</span>',
          10000
        )
        sseData(res, 'over', new Date(), '结束', 10000)
        return
      }
      queueExcute(res, fileList.length, result)
    } else {
      queueExcute(res, 0, result)
    }
  } catch (error) {
    console.log(error)
    sseData(res, 'error', new Date(), JSON.stringify(error))
    res.end()
  }
}

// 获取目录信息
const getDirData = async (res, path) => {
  res.writeHead(200, {
    'Content-Type': 'application/json',
    'Cache-Control': 'no-cache',
    Connection: 'keep-alive',
    'Access-Control-Allow-Origin': '*',
  })

  const parseUrl = url.parse(path, true)
  const { videoDir = '' } = parseUrl.query

  let fileList
  if (videoDir) {
    fileList = fs.readdirSync(videoDir).map((file) => videoDir + '/' + file)
  } else {
    fileList = fs.readdirSync(VIDEO_PATH).map((file) => VIDEO_PATH + '/' + file)
  }

  const data = {
    videoDir: fileList,
  }

  res.end(JSON.stringify(data))
}

// 60秒记录一次播放信息到文件
const savePlay = () => {
  let timer
  const fileList = fs.readdirSync(DATA_PATH)
  if (!fileList.includes('playRecorder.json')) {
    fs.writeFile(`${DATA_PATH}/playRecorder.json`, '{}', 'utf8', (err) => {
      if (err) throw err
    })
  }
  return () => {
    if (timer) return
    timer = setInterval(() => {
      const content = fs.readFileSync(`${DATA_PATH}/playRecorder.json`, 'utf8')
      const res = JSON.parse(content)
      Object.keys(playInfo).forEach((key) => {
        if (!res[key]) {
          res[key] = playInfo[key]
        } else {
          Object.assign(res[key], playInfo[key])
        }
      })
      fs.writeFile(
        `${DATA_PATH}/playRecorder.json`,
        JSON.stringify(res),
        'utf8',
        (err) => {
          if (err) throw err
        }
      )
    }, 60 * 1000)
  }
}

const startSave = savePlay()

// 记录播放时长
const recordPlay = async (res, path) => {
  res.writeHead(200, {
    'Content-Type': 'application/json',
    'Cache-Control': 'no-cache',
    Connection: 'keep-alive',
    'Access-Control-Allow-Origin': '*',
  })

  const parseUrl = url.parse(path, true)
  const { name, id, time } = parseUrl.query

  if (playInfo[name]) {
    const item = playInfo[name]
    item[id] = time
    item.last = id
  } else {
    const item = { [id]: time, last: id }
    playInfo[name] = item
  }

  startSave()

  res.end(JSON.stringify({ code: 'success' }))
}

// 获取播放时长
const getPlayRecord = (res, path) => {
  res.writeHead(200, {
    'Content-Type': 'application/json',
    'Cache-Control': 'no-cache',
    Connection: 'keep-alive',
    'Access-Control-Allow-Origin': '*',
  })

  const parseUrl = url.parse(path, true)
  const { name, id } = parseUrl.query

  const content = fs.readFileSync(`${DATA_PATH}/playRecorder.json`, 'utf8')
  const response = JSON.parse(content)

  const data = {
    time: response?.[name]?.[id] || 0,
  }

  res.end(JSON.stringify(data))
}

// 获取视频最后观看集数
const getPlayLast = (res, path) => {
  res.writeHead(200, {
    'Content-Type': 'application/json',
    'Cache-Control': 'no-cache',
    Connection: 'keep-alive',
    'Access-Control-Allow-Origin': '*',
  })

  const parseUrl = url.parse(path, true)
  const { name } = parseUrl.query

  const content = fs.readFileSync(`${DATA_PATH}/playRecorder.json`, 'utf8')
  const response = JSON.parse(content)

  const data = {
    last: response?.[name]?.last || 1,
  }

  res.end(JSON.stringify(data))
}

// 新建目录
const createDir = (res, path) => {
  res.writeHead(200, {
    'Content-Type': 'application/json',
    'Cache-Control': 'no-cache',
    Connection: 'keep-alive',
    'Access-Control-Allow-Origin': '*',
  })

  const parseUrl = url.parse(path, true)
  const { videoName } = parseUrl.query
  
  if (!videoName || videoName === 'undefined') {
    return res.end(JSON.stringify({ code: 0 }))
  }

  // 创建视频名称目录
  fs.mkdir(`${VIDEO_PATH}/${videoName}`, (err) => {
    if (err) {
      return res.end(JSON.stringify({ code: 0 }))
    }
    res.end(JSON.stringify({ code: 1 }))
  })
}

const getAsset = (res, src, req) => {
  let pathname = url.parse(src).pathname
  if (pathname === '/') pathname = 'index.html'

  const range = req.headers.range
  if (range) {
    const fileSize = fs.statSync(
      path.join('./', decodeURIComponent(pathname))
    ).size
    const parts = range.replace(/bytes=/, '').split('-')
    const start = parseInt(parts[0], 10)
    const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1
    const chunkSize = end - start + 1

    res.writeHead(206, {
      'Accept-Ranges': 'bytes',
      'Content-Type': 'video/mp4',
      'Content-Range': `bytes ${start}-${end}/${fileSize}`,
      'Content-Length': chunkSize,
    })
    const stream = fs.createReadStream(
      path.join('./', decodeURIComponent(pathname)),
      { start, end }
    )
    stream.pipe(res)
  } else {
    fs.readFile(path.join('./', decodeURIComponent(pathname)), (err, data) => {
      if (err) {
        res.writeHead(500, { 'Content-Type': 'text/plain' })
        res.end('Internal Server Error')
        console.log(err)
      } else {
        let type = ''
        if (pathname.endsWith('.css')) {
          type = 'text/css'
        } else if (pathname.endsWith('.js')) {
          type = 'text/javascript'
        }
        res.writeHead(200, { 'Content-Type': type })
        res.end(data)
      }
    })
  }
}

export default {
  excute,
  getDirData,
  recordPlay,
  getPlayRecord,
  getPlayLast,
  createDir,
  getAsset,
}
